package ca.uvic.cs.cogz.mapping.ui.tree;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.DefaultCellEditor;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.RepaintManager;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.TreeUI;
import javax.swing.plaf.UIResource;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import ca.uvic.cs.cogz.mapping.core.AbstractMapElement;
import ca.uvic.cs.cogz.mapping.core.MappingTypeManager.MappingType;
import ca.uvic.cs.cogz.mapping.filter.TreeFilterManager;
import ca.uvic.cs.cogz.mapping.ontology.DataLoaderFacade;
import ca.uvic.cs.cogz.mapping.ontology.TransformationEngine;
import ca.uvic.cs.cogz.mapping.ui.MappingVisualizationPanel;
import ca.uvic.cs.cogz.mapping.ui.renderer.MappingJTreeRenderer;
import ca.uvic.cs.cogz.util.TreeUtil;
import edu.stanford.smi.protege.model.Cls;
import edu.stanford.smi.protege.model.Frame;
import edu.stanford.smi.protege.model.KnowledgeBase;
import edu.stanford.smi.protege.util.LazyTreeModel;
import edu.stanford.smi.protege.util.LazyTreeNode;
import edu.stanford.smi.protege.util.LazyTreeRoot;
import edu.stanford.smi.protege.util.SelectableTree;

/**
 * Extends Protege's SelectableTree component so that I can overload some of the paint related
 * methods to handle my fancy zooming and fish-eye effects.
 * 
 * @author seanf
 */
public class MappingJTree extends SelectableTree {
	private static final long serialVersionUID = 3341148309997483169L;

	private int treeType;

	private KnowledgeBase knowledgeBase;

	/** reference to the PromptPanel, which is the main panel which embeds this tree */
	private MappingVisualizationPanel promptPanel;

	/** tree zoom managers */
	private ZoomManager zoomManager;

	/** custom tree UIs to handle some of the zooming effects */
	private ZoomableTreeUI zoomTreeUI;

	/** custom renderer object for displaying mapping information and tree filtering effects */
	private MappingJTreeRenderer mapRenderer;

	/** the currently selected path */
	private TreePath currentPath;

	/** the original root element for this tree */
	private Cls originalRoot;

	private JScrollPane scrollPane;
	
	private List<ChangeListener> resetListeners;

	/** menu options */
	private JPopupMenu rightClickMenu;
	private Action instantiateClassAction;
	private Action resetTreeAction;
	private Action removeBranchFiltersAction;
	private Action setFocusAction;
	private Action filterBranchAction;
	private Action pinBranchAction;
	private Action unpinBranchAction;

	public MappingJTree(MappingVisualizationPanel promptPanel, Cls rootCls, Map<Frame, MappingType> mappedFrames, KnowledgeBase knowledgeBase) {
		super(null, null);
		
		this.knowledgeBase = knowledgeBase;
		this.promptPanel = promptPanel;
		this.originalRoot = rootCls;
		this.zoomManager = new ZoomManager(this.getFont());
		this.resetListeners = new ArrayList<ChangeListener>();

		if (knowledgeBase.equals(DataLoaderFacade.getInstance().getSourceOntology())) {
			treeType = TreeFilterManager.SOURCE_SEARCH;
		} else {
			treeType = TreeFilterManager.TARGET_SEARCH;
		}

		this.mapRenderer = new MappingJTreeRenderer(mappedFrames, zoomManager, treeType);

		init();
	}
	
	public void addResetListener(ChangeListener changeListener) {
		resetListeners.add(changeListener);
	}

	public void processMouseWheelEvent(MouseWheelEvent e) {
		super.processMouseWheelEvent(e);
	}

	public ZoomableTreeUI getZoomTreeUI() {
		return zoomTreeUI;
	}

	public ZoomManager getZoomManager() {
		return zoomManager;
	}

	public boolean isSource() {
		return treeType == TreeFilterManager.SOURCE_SEARCH;
	}

	/**
	 * Initialize the tree by creating a new root and setting the model.
	 */
	public void initTree() {
		TreeFilterManager.getInstance().reset();

		Cls rootCls = knowledgeBase.getRootCls();
		LazyTreeModel model = (LazyTreeModel) this.getModel();
		LazyTreeRoot root = new FilteredParentChildRoot(rootCls);
		model.setRoot(root);

		this.setModel(model); // force it to update itself
		this.expandRow(getExpansionRow());
	}

	/**
	 * Re-initializes the tree and expands paths based on the results collection if expand is true.
	 */
	@SuppressWarnings("unchecked")
	public void initTree(Collection<Collection> results, boolean expand) {
		initTree(getModel().getRoot(), results, expand);
	}
	
	public void removeTreePath(ArrayList<AbstractMapElement> mapElements) {
		if (mapElements == null) {
			return;
		}

		ArrayList<TreePath> treePaths = new ArrayList<TreePath>();
		for (AbstractMapElement mapElement : mapElements) {
			TreePath treePath = null;
			if (isSource()) {
				treePath = TreeUtil.getTreePath(this, mapElement.getSourcePath());
			} else {
				treePath = TreeUtil.getTreePath(this, mapElement.getTargetPath());
			}

			if (treePath != null) {
				treePaths.add(treePath);
			}
		}

		// select all matching mapping paths and scroll to the first one
		if (treePaths != null && treePaths.size() > 0) {
			TreePath[] treePathsAsArray = treePaths.toArray(new TreePath[treePaths.size()]);
			this.removeSelectionPaths(treePathsAsArray);
		}
	}

	/**
	 * Selects all TreePaths that are part of the mapElements list.
	 * 
	 * @return True if some item was selected, otherwise False.
	 */
	public boolean selectTreePath(ArrayList<AbstractMapElement> mapElements, boolean overwriteSelectionPaths) {
		if (mapElements == null) {
			return false;
		}

		ArrayList<TreePath> treePaths = new ArrayList<TreePath>();
		for (AbstractMapElement mapElement : mapElements) {
			TreePath treePath = null;
			if (isSource()) {
				treePath = TreeUtil.getTreePath(this, mapElement.getSourcePath());
			} else {
				treePath = TreeUtil.getTreePath(this, mapElement.getTargetPath());
			}

			if (treePath != null) {
				treePaths.add(treePath);
			}
		}

		// select all matching mapping paths and scroll to the first one
		if (treePaths != null && treePaths.size() > 0) {
			TreePath[] treePathsAsArray = treePaths.toArray(new TreePath[treePaths.size()]);
			if (!overwriteSelectionPaths) {
				this.addSelectionPaths(treePathsAsArray);
			} else {
				this.setSelectionPaths(treePathsAsArray);
			}
			this.scrollPathToVisible(treePaths.get(0));
		}

		return treePaths.size() > 0;
	}

	public void updateUI() {
		// @tag note : this is the change made to this method, it forces the tree to not reset it's
		// UI
		if (this.getUI() == null) {
			setUI((TreeUI) UIManager.getUI(this));
		}

		TreeCellRenderer renderer = getCellRenderer();
		if (!(renderer instanceof UIResource) && renderer instanceof Component) {
			SwingUtilities.updateComponentTreeUI((Component) renderer);
		}

		TreeCellEditor editor = getCellEditor();
		if (!(editor instanceof UIResource)) {
			if (editor instanceof Component) {
				SwingUtilities.updateComponentTreeUI((Component) editor);
			} else if (editor instanceof DefaultCellEditor) {
				Component comp = ((DefaultCellEditor) editor).getComponent();
				if (comp != null) {
					SwingUtilities.updateComponentTreeUI(comp);
				}
			}
		}

	}

	public void repaint(long tm, int x, int y, int width, int height) {
		if (promptPanel != null) {
			RepaintManager.currentManager(this).markCompletelyDirty(promptPanel.getArcsPanel());
		} else {
			super.repaint(tm, x, y, width, height);
		}
	}

	public String getToolTipText(MouseEvent evt) {
		if (getRowForLocation(evt.getX(), evt.getY()) == -1) {
			return null;
		}
		TreePath curPath = getPathForLocation(evt.getX(), evt.getY());
		if(curPath.getLastPathComponent() instanceof IFilteredTreeNode) {
			return ((IFilteredTreeNode) curPath.getLastPathComponent()).getToolTipText();
		}
		
		return "";
	}

	public void refreshTree() {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				zoomTreeUI.updateSize();
				updateTreeRowHeights();
			}
		});
	}
	
	private int getExpansionRow() {
		if(DataLoaderFacade.getInstance().isShowSlots()) return 1;
		return 0;
	}

	@SuppressWarnings("unchecked")
	private void initTree(Object rootNode, Collection<Collection> results, boolean expand) {
		Collection<Collection> selection = TreeUtil.saveSelection(this);
		Collection<Collection> expansion = TreeUtil.saveExpansion(this);

		LazyTreeModel model = (LazyTreeModel) this.getModel();
		LazyTreeRoot root = (LazyTreeRoot) rootNode;

		model.setRoot(root);
		root.reload();
		this.setModel(model); // force it to update itself

		if (results != null && expand) {
			TreeUtil.expandPaths(this, results);
		}

		// restore previous tree state
		TreeUtil.restoreSelection(this, selection);
		TreeUtil.expandPaths(this, expansion);
		
		if(results == null && expansion.size() == 0) {
			expandRow(getExpansionRow());
		}
	}
	
	private void fireResetListeners() {
		for(ChangeListener listener : resetListeners) {
			listener.stateChanged(null);
		}
	}

	/**
	 * Resets the tree row heights in order to force the tree to update the node sizes.
	 * 
	 * @param tree
	 */
	private void updateTreeRowHeights() {
		if (getRowHeight() <= 0) {
			setRowHeight(1);
		}
		setRowHeight(0);
		repaint();
	}

	private void setNodeAsRoot() {
		if (currentPath != null) {
			LazyTreeNode treeNode = (LazyTreeNode) currentPath.getLastPathComponent();
			Cls rootCls = (Cls) treeNode.getUserObject();

			setTreeRoot(rootCls);
		}
	}

	private void filterNode() {
		if (currentPath != null) {
			Frame frame = TreeUtil.getLastSelectedUserObject(currentPath);

			TreeFilterManager.getInstance().setActiveSearch(treeType);
			TreeFilterManager.getInstance().performFrameFilter(frame, true);

			initTree(null, true);
		}
	}

	private void showBranch() {
		if (currentPath != null) {
			Frame frame = TreeUtil.getLastSelectedUserObject(currentPath);

			TreeFilterManager.getInstance().setActiveSearch(treeType);
			TreeFilterManager.getInstance().performFrameFilter(frame, false);

			LazyTreeNode treeNode = (LazyTreeNode) currentPath.getLastPathComponent();
			treeNode.reload();
		}
	}

	private void removePin() {
		if (currentPath != null) {
			Frame frame = TreeUtil.getLastSelectedUserObject(currentPath);

			TreeFilterManager.getInstance().removePinnedFrame(treeType, frame);

			LazyTreeNode treeNode = (LazyTreeNode) currentPath.getLastPathComponent();
			treeNode.reload();
		}
	}
	
	/**
	 * Instantiates the class inside a transformation.
	 */
	private void instantiateClass() {
		if (currentPath != null) {
			Frame frame = TreeUtil.getLastSelectedUserObject(currentPath);
		
			if(frame instanceof Cls) {
				Cls cls = (Cls)frame;
				TransformationEngine.getInstance().instantiateClass(cls);
			}
		}
	}

	private void removeNodeFilters() {
		TreeFilterManager.getInstance().setActiveSearch(treeType);
		TreeFilterManager.getInstance().resetFrameFilters();

		initTree(null, true);
	}

	public void resetTree() {
		TreeFilterManager.getInstance().resetSearch(treeType);
		TreeFilterManager.getInstance().setActiveSearch(treeType);

		setTreeRoot(originalRoot);
		fireResetListeners();
	}

	private void setTreeRoot(Cls rootCls) {
		TreeFilterManager.getInstance().setActiveSearch(treeType);

		initTree(new FilteredParentChildRoot(rootCls), null, false);

		expandRow(getExpansionRow());
	}

	private void init() {
		createActions();

		zoomTreeUI = new ZoomableTreeUI(zoomManager);

		this.setUI(zoomTreeUI);
		this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
		this.addMouseListener(new RightClickSelector(this));
		this.setCellRenderer(mapRenderer);
		this.setRowHeight(0);
		this.setToolTipText("");
		this.setShowsRootHandles(true);
		
		zoomManager.setTree(this);
		createTreeContextMenu();

		this.addFocusListener(new FocusAdapter() {
			public void focusGained(FocusEvent e) {
				TreeFilterManager.getInstance().setActiveSearch(treeType);
			}
		});
	}

	private void createActions() {
		instantiateClassAction = new AbstractAction("Instatiate", null) {
			private static final long serialVersionUID = 1L;

			public void actionPerformed(ActionEvent e) {
				instantiateClass();
			}
		};
		
		// right-click menu options for trees
		resetTreeAction = new AbstractAction("Reset", null) {
			private static final long serialVersionUID = -6646622590256617904L;

			public void actionPerformed(ActionEvent e) {
				resetTree();
			}
		};
		resetTreeAction.putValue(Action.SHORT_DESCRIPTION, "Reset");

		setFocusAction = new AbstractAction("Set focus", null) {
			private static final long serialVersionUID = 4657590156450823837L;

			public void actionPerformed(ActionEvent e) {
				setNodeAsRoot();
			}
		};
		setFocusAction.putValue(Action.SHORT_DESCRIPTION, "Set focus");

		filterBranchAction = new AbstractAction("Filter branch", null) {
			private static final long serialVersionUID = 4657590156450823837L;

			public void actionPerformed(ActionEvent e) {
				filterNode();
			}
		};
		filterBranchAction.putValue(Action.SHORT_DESCRIPTION, "Filter branch");

		pinBranchAction = new AbstractAction("Pin branch", null) {
			private static final long serialVersionUID = 4657590156450823837L;

			public void actionPerformed(ActionEvent e) {
				showBranch();
			}
		};
		pinBranchAction.putValue(Action.SHORT_DESCRIPTION, "Pin branch");

		unpinBranchAction = new AbstractAction("Remove pin", null) {
			private static final long serialVersionUID = 4657590156450823837L;

			public void actionPerformed(ActionEvent e) {
				removePin();
			}
		};
		unpinBranchAction.putValue(Action.SHORT_DESCRIPTION, "Remove pin");
		unpinBranchAction.setEnabled(false);

		removeBranchFiltersAction = new AbstractAction("Remove branch filters", null) {
			private static final long serialVersionUID = 4657590156450823837L;

			public void actionPerformed(ActionEvent e) {
				removeNodeFilters();
			}
		};
		removeBranchFiltersAction.putValue(Action.SHORT_DESCRIPTION, "Remove branch filters");
	}

	private void setMenuItemState() {
		instantiateClassAction.setEnabled(false);
		
		if (currentPath != null) {
			Frame frame = TreeUtil.getLastSelectedUserObject(currentPath);
			LazyTreeNode treeNode = (LazyTreeNode) currentPath.getLastPathComponent();
			
			if(frame.getKnowledgeBase().equals(DataLoaderFacade.getInstance().getTargetOntology())) {
				instantiateClassAction.setEnabled(true);
			}

			unpinBranchAction.setEnabled(false);
			pinBranchAction.setEnabled(false);
			if (TreeFilterManager.getInstance().isFramePinned(treeType, frame)) {
				unpinBranchAction.setEnabled(true);
				pinBranchAction.setEnabled(false);
			} else if (((Cls) frame).getDirectSubclassCount() > treeNode.getChildCount()) {
				pinBranchAction.setEnabled(true);
			}
		}
	}

	private void createTreeContextMenu() {
		rightClickMenu = new JPopupMenu();
		rightClickMenu.add(instantiateClassAction);
		rightClickMenu.addSeparator();
		rightClickMenu.add(setFocusAction);
		rightClickMenu.addSeparator();
		rightClickMenu.add(pinBranchAction);
		rightClickMenu.add(unpinBranchAction);
		rightClickMenu.addSeparator();
		rightClickMenu.add(filterBranchAction);
		rightClickMenu.add(removeBranchFiltersAction);
		rightClickMenu.addSeparator();
		rightClickMenu.add(resetTreeAction);
	}

	public void setScrollPane(JScrollPane scrollPane) {
		this.scrollPane = scrollPane;
	}

	public JScrollPane getScrollPane() {
		return scrollPane;
	}

	/**
	 * Listens for a mouse event on a tree and selects the tree path on right mouse button press.
	 * This is used so that when a context menus is shown the selection has already been set.
	 * 
	 * @author Chris Callendar
	 * @date 15-Nov-06
	 */
	class RightClickSelector extends MouseAdapter {
		private JTree tree;

		public RightClickSelector(JTree tree) {
			this.tree = tree;
		}

		@Override
		public void mousePressed(MouseEvent e) {
			if (SwingUtilities.isRightMouseButton(e)) {
				TreePath path = tree.getPathForLocation(e.getX(), e.getY());
				if (path != null) {
					tree.addSelectionPath(path);
					currentPath = path;

					// do not display this menu for the root node
					if (TreeUtil.getLastSelectedUserObject(path) instanceof Cls && !((Cls) TreeUtil.getLastSelectedUserObject(path)).isRoot()) {
						setMenuItemState();
						rightClickMenu.show(MappingJTree.this, e.getX(), e.getY());
					}
				}
			}
		}
	}
}
